/**
 * Copyright (c) 2020 Coder League
 * All rights reserved.
 *
 * File：WeixinInterfaceService.java
 * History:
 *         2020年12月15日: Initially created, Chrise.
 */
package club.coderleague.cfss.service.interfaces;

import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.fasterxml.jackson.databind.ObjectMapper;

import club.coderleague.cfss.cache.CacheManager;
import club.coderleague.cfss.config.properties.WeixinSettings;
import club.coderleague.cfss.domain.beans.SystemConfig;
import club.coderleague.cfss.domain.beans.UserSession;
import club.coderleague.cfss.domain.beans.WebAuthContext;
import club.coderleague.cfss.domain.beans.wxmp.AbstractMessage;
import club.coderleague.cfss.handler.wxmp.MessageHandler;
import club.coderleague.cfss.service.user.UserService;
import club.coderleague.cfss.util.HttpUtil;
import club.coderleague.security.AlgorithmBeanFactory;
import club.coderleague.security.algorithm.SHA1Signer;

/**
 * 微信接口服务。
 * @author Chrise
 */
@Service
public class WeixinInterfaceService {
	private static final Logger logger = LoggerFactory.getLogger(WeixinInterfaceService.class);
	private static final String CHARSET = "UTF-8";
	private static final String AUTH_STATE_KEY = "auth_state";
	private static final String AUTH_CODE_KEY = "auth_code";
	private static final String LOGIN_REQUEST_URL = "/mobile/login.xhtml";
	private static final String SUPPLIER_REQUEST_URL = "/mobile/supplier/main.xhtml";
	private static final String CYBERCAFE_REQUEST_URL = "/mobile/cybercafe/main.xhtml";
	
	@Autowired
	private WeixinSettings weixinSettings;
	@Autowired
	private CacheManager cacheManager;
	@Autowired
	private UserService userService;
	private Map<String, MessageHandler> messageHandlers = new ConcurrentHashMap<String, MessageHandler>();
	
	/**
	 * 添加消息处理器。
	 * @author Chrise 2020年12月19日
	 * @param handler 处理器对象。
	 */
	public void addMessageHandler(MessageHandler handler) {
		this.messageHandlers.put(handler.type(), handler);
	}
	
	/**
	 * 验证开发者身份。
	 * @author Chrise 2020年12月23日
	 * @param signature 签名。
	 * @param timestamp 时间戳。
	 * @param nonce 随机数。
	 * @param echostr 随机字符串。
	 * @param response 响应对象。
	 */
	public void verifyDeveloperIdentity(String signature, String timestamp, String nonce, String echostr, HttpServletResponse response) {
		try {
			// 获取系统配置
			SystemConfig sc = (SystemConfig)this.cacheManager.getObject(SystemConfig.CACHE_KEY, false);
			if (sc == null) return;
			
			// 排序并连接验证参数
			String[] array = {sc.getWxvertoken(), timestamp, nonce};
			Arrays.sort(array);
			StringBuilder content = new StringBuilder();
			for (String str : array) {
				content.append(str);
			}
			
			// 验证签名
			String local = AlgorithmBeanFactory.getAlgorithmBean(SHA1Signer.class).sign(content.toString());
			if (local.equals(signature)) {
				// 原样返回随机参数
				response.getWriter().print(echostr);
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}
	}
	
	/**
	 * 分发消息。
	 * @author Chrise 2020年12月23日
	 * @param request 请求对象。
	 * @param response 响应对象。
	 */
	public void dispatchMessage(HttpServletRequest request, HttpServletResponse response) {
		try {
			// 设置编码
			request.setCharacterEncoding(CHARSET);
			response.setCharacterEncoding(CHARSET);
			
			// 解析消息
			AbstractMessage msg = AbstractMessage.parse(request.getInputStream());
			if (msg == null) {
				logger.error("The message receive correctly but could not be parsed. -> [{}]", msg);
				response.getWriter().write("");
				return;
			}
			
			// 查找消息处理器
			MessageHandler handler = this.messageHandlers.get(msg.getMsgType());
			if (handler == null) {
				logger.error("The message receive correctly but handler not found. -> [{}]", msg);
				response.getWriter().write("");
				return;
			}
			
			// 处理消息
			String reply = handler.handle(msg);
			if (reply == null) {	// 系统无法立即返回处理结果
				logger.error("The message handle correctly but could not be replied immediately. -> [{}]", msg);
				reply = "";
			}
			
			// 回复处理结果
			response.getWriter().write(reply);
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}
	}
	
	/**
	 * 获取授权地址。
	 * @author Chrise 2020年12月23日
	 * @param session 会话对象。
	 * @return 授权地址。
	 */
	public String getAuthUrl(HttpSession session) {
		try {
			// 获取系统配置
			SystemConfig sc = (SystemConfig)this.cacheManager.getObject(SystemConfig.CACHE_KEY, false);
			if (sc == null) return "";
			
			// 缓存时间戳作为校验码
			long timestamp = System.currentTimeMillis();
			session.setAttribute(AUTH_STATE_KEY, timestamp);
			
			this.cacheAuthCode();	// 缓存授权代码
			
			// 构建授权地址
			String redirect = URLEncoder.encode(this.weixinSettings.getWebAuthCallbackRequest(), "UTF-8");
			String url = String.format(this.weixinSettings.getWebAuthCodeRequest(), sc.getWxappid(), redirect, timestamp);
			
			return url;
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}
		
		return null;
	}
	
	/**
	 * 获取开放标识。
	 * @author Chrise 2020年12月23日
	 * @param code 授权代码。
	 * @param state 状态。
	 * @param session 会话对象。
	 * @return 开放标识。
	 */
	@SuppressWarnings("unchecked")
	public String getOpenId(String code, String state, HttpSession session) {
		Object timestamp = null;
		
		try {
			// 验证授权校验码
			timestamp = session.getAttribute(AUTH_STATE_KEY);
			if (timestamp == null || 
				!timestamp.toString().equals(state) || 
				(System.currentTimeMillis() - ((Long)timestamp).longValue()) > this.weixinSettings.getWebAuthCallbackTimeout()) return null;
			
			// 获取系统配置
			SystemConfig sc = (SystemConfig)this.cacheManager.getObject(SystemConfig.CACHE_KEY, false);
			if (sc == null) return null;
			
			// 请求开放标识
			String url = String.format(this.weixinSettings.getWebAuthTokenRequest(), sc.getWxappid(), sc.getWxappkey(), code);
			String result = HttpUtil.get(url);
			if (result != null) {
				// 解析并验证请求结果
				Map<String, Object> map = new ObjectMapper().readValue(result, Map.class);
				if (map.containsKey("openid")) return map.get("openid").toString();
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		} finally {
			if (timestamp != null) session.removeAttribute(AUTH_STATE_KEY);
		}
		
		return null;
	}
	
	/**
	 * 授权成功事件。
	 * @author Chrise 2020年12月23日
	 * @param openId 开放标识。
	 * @param session 会话对象。
	 * @return 重定向地址。
	 */
	public String onAuthSuccess(String openId, HttpSession session) {
		try {
			// 获取网页授权上下文
			WebAuthContext context = (WebAuthContext)session.getAttribute(WebAuthContext.SESSION_KEY);
			if (context == null) return null;
			
			// 生成重定向地址
			String redirect = null;
			String domain = this.weixinSettings.getDomain();
			switch (context.getScene()) {
				case SUPPLIER_SYSTEM:
					redirect = domain + SUPPLIER_REQUEST_URL;
					break;
				case CYBERCAFE_SYSTEM:
					redirect = domain + CYBERCAFE_REQUEST_URL;
					break;
				default:
					return null;
			}
			
			// 处理已存在的用户会话
			UserSession exists = (UserSession)session.getAttribute(UserSession.SESSION_KEY);
			if (exists != null) {
				if (openId.equals(exists.getOpenid())) return redirect;
				else session.removeAttribute(UserSession.SESSION_KEY);
			}
			
			// 检查开放标识是否已绑定
			UserSession us = this.userService.execCheckBound(context.getScene(), openId);
			if (us == null) {
				// 缓存开放标识
				context.setOpenid(openId);
				context.setRedirect(redirect);
				session.setAttribute(WebAuthContext.SESSION_KEY, context);
				return domain + LOGIN_REQUEST_URL;
			}
			
			// 切换会话
			session.removeAttribute(WebAuthContext.SESSION_KEY);
			session.setAttribute(UserSession.SESSION_KEY, us);
			
			return redirect;
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}
		
		return null;
	}
	
	/**
	 * 获取授权回调地址（模拟微信公众平台）。
	 * @author Chrise 2020年12月23日
	 * @param redirect 重定向地址。
	 * @param state 状态。
	 * @param request 请求对象。
	 * @return 授权回调地址。
	 */
	public String getAuthCallbackUrl(String redirect, String state, HttpServletRequest request) {
		// 未启用模拟器时拒绝请求
		if (!this.weixinSettings.getWebAuthSimulatorEnabled()) return null;
		
		// 获取授权代码
		String code = this.getCachedAuthCode(request);
		if (code == null) return null;
		
		// 构建授权回调地址
		String url = redirect + "?code=" + code + "&state=" + state;
		return url;
	}
	
	/**
	 * 获取开放标识（模拟微信公众平台）。
	 * @author Chrise 2020年12月23日
	 * @param code 授权代码。
	 * @return 开放标识。
	 */
	public Map<String, Object> getOpenId(String code) {
		Map<String, Object> response = new HashMap<>();
		
		// 启用模拟器时生成开放标识
		if (this.weixinSettings.getWebAuthSimulatorEnabled()) {
			String openId = String.format("wx%s%s", new SimpleDateFormat("yy").format(new Date()), Base64.encodeBase64URLSafeString(code.getBytes()));
			response.put("openid", openId);
		}
		
		return response;
	}
	
	/**
	 * 缓存授权代码（模拟微信公众平台）。
	 * @author Chrise 2020年12月23日
	 */
	private void cacheAuthCode() {
		// 启用模拟器时缓存授权代码
		if (this.weixinSettings.getWebAuthSimulatorEnabled()) {
			ServletRequestAttributes sra = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
			HttpServletRequest request = sra.getRequest();
			HttpServletResponse response = sra.getResponse();
			
			// 获取已缓存的授权代码
			String value = this.getCachedAuthCode(request);
			if (value == null) value = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
			
			// 重新缓存授权代码
			Cookie cookie = new Cookie(AUTH_CODE_KEY, value);
			cookie.setMaxAge(10 * 365 * 24 * 60 * 60);
			cookie.setPath(request.getContextPath());
			response.addCookie(cookie);
		}
	}
	
	/**
	 * 获取缓存的授权代码（模拟微信公众平台）。
	 * @author Chrise 2020年12月23日
	 * @param request 请求对象。
	 * @return 授权代码。
	 */
	private String getCachedAuthCode(HttpServletRequest request) {
		// 读取COOKIE对象
		Cookie[] cookies = request.getCookies();
		if (cookies == null || cookies.length <= 0) return null;
		
		// 从缓存中查找授权代码
		for (Cookie cookie : cookies) {
			if (AUTH_CODE_KEY.equals(cookie.getName())) return cookie.getValue();
		}
		
		return null;
	}
}
